import { Browser, ElementHandle, Page } from 'puppeteer';
import puppeteer from 'puppeteer-extra';
import { NodeFor } from 'puppeteer';
import randomUseragent from 'random-useragent';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { IScraperProp, PrismaService } from './types';
import { Logger } from '@nestjs/common';
import { randomInt } from 'crypto';
import { CreateGoodsDto } from 'src/goods/dto/create-goods.dto';
puppeteer.use(StealthPlugin());

export class Scraper {
  url: string;
  keyword: string;
  cookie: string | undefined;
  prePage: Page | null;
  browser: Browser | null;
  dbService: PrismaService | null;
  page: Page | null;
  selector: string | null;
  constructor({ url, keyword, ...props }: IScraperProp) {
    this.url = url;
    this.keyword = keyword;
    this.dbService = props.dbService || null;
    this.cookie = props.cookie;
    this.selector = props.selector || null;
  }
  async rowScrap(): Promise<string | null> {
    const page = await this.launchBrowser();
    await page.goto(this.url, { waitUntil: 'load' });
    await this.delayRandomly();
    if (this.selector) {
      return await page.$eval(this.selector, (el) => el.textContent);
    } else {
      return await page.$eval('body', (el) => el.textContent);
    }
  }

  //模拟人类操作，打开新页面
  async initNewTab(page: Page, url: string): Promise<Page> {
    try {
      await page.setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
      );

      await page.setViewport({
        width: 1366,
        height: 768,
        deviceScaleFactor: 1,
      });

      await page.evaluateOnNewDocument(() => {
        Object.defineProperty(navigator, 'webdriver', { get: () => false });
        Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
        Object.defineProperty(window, 'screen', {
          get: () => ({
            width: 1366,
            height: 768,
            availWidth: 1366,
            availHeight: 768,
          }),
        });
      });

      await page.setExtraHTTPHeaders({
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        Accept:
          'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        Referer: url,
        Connection: 'keep-alive',
      });

      await page.mouse.move(Math.random() * 800, Math.random() * 600, {
        steps: 10,
      });
      return page;
    } catch (error) {
      console.error('模拟人类打开标签失败:', error);
      throw error;
    }
  }

  async initPage(page: Page): Promise<Page> {
    await page?.setRequestInterception(true);
    page?.on('request', (request) => {
      if (['image', 'stylesheet'].includes(request.resourceType())) {
        request.abort();
      } else {
        request.continue();
      }
    });

    this.page = page;
    return this.page;
  }
  async closePage() {
    await this.page?.close();
    this.page = null;
  }
  async store(data: CreateGoodsDto[]) {
    if (!this.dbService) {
      Logger.error(`db service not initialied`);
      return;
    }
    await this.dbService.storeGoods(data);
  }
  async scraping() {
    Logger.error('base scraper not impletement this function');
  }
  async launchBrowser(headless: boolean = true): Promise<Page> {
    const runMode = process.env.NODE_ENV;
    Logger.warn(`run in ${runMode}`);
    if (runMode === 'production') {
      headless = true;
    }
    this.browser = await puppeteer.launch({
      headless: headless,
      args: [
        '--no-sandbox',
        '--disable-gpu',
        '--disable-setuid-sandbox',
        '--disable-infobars',
        '--window-size=1366,768', // 设置窗口大小
        '--start-maximized', // 最大化窗口
      ],
    });
    this.browser.once('targetcreated', async (target) => {
      if (target.type() === 'page') {
        const page = await target.page();
        if (!page) {
          Logger.log('New page create failed');
        }
      }
    });
    if (this.cookie) {
      Logger.log('set cookie');
      try {
        const cookieobj = JSON.parse(this.cookie);
        await this.browser.setCookie(...cookieobj);
      } catch (err) {
        Logger.error(err);
      }
    } else {
      Logger.log('with no cookie');
    }

    // Initial page
    this.prePage = await this.browser.newPage();
    // return await this.initPage(this.prePage);
    return await this.initNewTab(this.prePage, this.url);
  }
  async clickOpenGoodsPage(
    fromPage: ElementHandle<NodeFor<string>>,
    refererUrl: string,
  ): Promise<Page> {
    // Promise to resolve with the new page
    const newPagePromise = new Promise<Page>((resolve) => {
      this.browser!.once('targetcreated', async (target) => {
        const newPage = await target.page();
        if (newPage) resolve(newPage);
      });
    });

    // Click the link on the given page
    await fromPage.click();
    await this.delayRandomly();
    // Wait for and return the new page
    const newPage = await newPagePromise;
    this.initNewTab(newPage, refererUrl);
    await newPage.waitForNetworkIdle({ idleTime: 2000 });
    await newPage.bringToFront();
    await this.delayRandomly();
    await this.mouseRandomly(newPage);
    await this.simulateHumanScroll(newPage, 1000, 2000);
    return newPage;
  }

  async newPage(): Promise<Page | null> {
    const page = await this.browser?.newPage();
    if (!page) return null;
    await page?.setViewport({ width: 1920, height: 1080 });
    // await page?.setRequestInterception(true);
    // page?.on('request', (request) => {
    //   if (['image', 'stylesheet'].includes(request.resourceType())) {
    //     request.abort();
    //   } else {
    //     request.continue();
    //   }
    // });
    return page;
  }
  async exit() {
    await this.page?.close();
    Logger.warn('browser will close');
    await this.browser?.close();
  }
  // Utility function for random delays with TypeScript typing
  async delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  async delayRandomly(
    minMs: number = 5000,
    maxMs: number = 15000,
  ): Promise<void> {
    if (minMs < 0 || maxMs < minMs) {
      throw new Error(
        '最小延迟必须大于或等于 0，且最大延迟必须大于或等于最小延迟',
      );
    }
    // 偏向较短延迟（示例：指数分布）
    const bias = 0.5; // 偏向因子（0-1 之间，接近 1 偏向最小值）
    const delayMs = Math.floor(
      minMs + (maxMs - minMs) * Math.pow(Math.random(), 1 / bias),
    );
    await this.delay(delayMs);
  }
  async scrollDown(): Promise<void> {
    if (!this.page) {
      Logger.error('no active page found');
      return;
    }
    await this.page.evaluate(() =>
      window.scrollTo(0, document.body.scrollHeight),
    );
  }
  async scrollUp(): Promise<void> {
    if (!this.page) {
      Logger.error('no active page found');
      return;
    }
    await this.page.evaluate(() => window.scrollTo(0, 0));
  }

  async mouseRandomly(page: Page): Promise<void> {
    const from = randomInt(301, 500);
    const to = randomInt(100, 300);
    // Logger.log(`random move ${from} to ${to}`);
    await this.page?.mouse.move(from, to, { steps: 10 });
  }
  async monitorHumanScroll(page: Page): Promise<void> {
    await page.evaluate(async () => {
      // 随机延迟函数，模拟人类操作的不规律性
      const randomDelay = (min: number, max: number) =>
        new Promise((resolve) =>
          setTimeout(resolve, Math.random() * (max - min) + min),
        );

      // 获取页面高度
      const getScrollHeight = () => document.body.scrollHeight;

      await new Promise<void>(async (resolve) => {
        let totalHeight = 0;
        let lastHeight = getScrollHeight();
        let stagnantCount = 0; // 页面高度无变化的次数，用于检测加载结束

        const scrollStep = async () => {
          const currentHeight = getScrollHeight();
          const windowHeight = window.innerHeight;

          // 随机滚动距离（50-200 像素），模仿人类的不规则滚动
          const distance = Math.floor(Math.random() * 150) + 50;
          window.scrollBy(0, distance);
          totalHeight += distance;

          // 检查是否接近底部
          if (totalHeight >= currentHeight - windowHeight) {
            // 页面高度变化，可能是动态加载
            if (currentHeight > lastHeight) {
              lastHeight = currentHeight;
              stagnantCount = 0; // 重置计数器
              await randomDelay(500, 1500); // 等待加载，500-1500ms
              scrollStep(); // 继续滚动
            } else {
              stagnantCount++;
              // 高度连续几次无变化，认为滚动到底
              if (stagnantCount >= 3) {
                resolve();
              } else {
                await randomDelay(300, 1000); // 短暂停顿后继续
                scrollStep();
              }
            }
          } else {
            // 随机停顿，模拟人类浏览
            if (Math.random() < 0.3) {
              // 30% 概率停顿
              await randomDelay(800, 2000); // 停顿 0.8-2 秒
            } else {
              await randomDelay(50, 200); // 正常滚动间隔 50-200ms
            }

            // 随机回滚，模拟人类行为
            if (Math.random() < 0.1) {
              // 10% 概率回滚
              window.scrollBy(0, -Math.floor(Math.random() * 100 + 50));
              await randomDelay(500, 1200);
            }
            scrollStep(); // 继续滚动
          }
        };
        // 开始滚动
        scrollStep();
      });
    });
    // 滚动结束后，等待2s
    this.delay(2000);
  }

  async simulateHumanScroll(page, minDistance: number, maxDistance: number) {
    if (minDistance < 0 || maxDistance < minDistance) {
      throw new Error(
        '最小距离必须大于或等于 0，且最大距离必须大于或等于最小距离',
      );
    }
    try {
      const pageHeight = await page.evaluate(() =>
        Math.max(
          document.body.scrollHeight,
          document.documentElement.scrollHeight,
        ),
      );
      const scrollDistance = Math.min(
        Math.floor(Math.random() * (maxDistance - minDistance + 1)) +
          minDistance,
        pageHeight,
      );
      // 滚动页面到目标位置
      await page.evaluate(async (distance) => {
        const segment = 100; // 每次滚动的距离（像素）
        const segments = Math.ceil(distance / segment); // 计算总共的滚动段数
        let currentPosition = 0;

        for (let i = 0; i < segments; i++) {
          const scrollAmount = Math.min(segment, distance - currentPosition);
          window.scrollBy(0, scrollAmount); // 滚动指定距离
          currentPosition += scrollAmount;

          // 模拟间隔时间
          await new Promise((resolve) =>
            setTimeout(resolve, Math.random() * (200 - 50) + 50),
          );
        }
      }, scrollDistance);

      // 停顿一段时间后，回滚到原位置
      await this.delayRandomly(3000, 5000); // 模拟用户的停顿

      // 滚动回到起始位置（或之前的某个位置）
      await page.evaluate(async (distance) => {
        const segment = 100; // 每次滚动的距离（像素）
        const segments = Math.ceil(distance / segment); // 计算回滚的段数
        let currentPosition = distance;

        for (let i = 0; i < segments; i++) {
          const scrollAmount = Math.min(segment, currentPosition);
          window.scrollBy(0, -scrollAmount); // 回滚指定距离
          currentPosition -= scrollAmount;

          // 模拟回滚间隔时间
          await new Promise((resolve) =>
            setTimeout(resolve, Math.random() * (200 - 50) + 50),
          );
        }
      }, scrollDistance);

      // 等待几秒钟再继续执行
      await this.delayRandomly(3000, 5000);
    } catch (err) {
      Logger.error('滑动浏览器失败');
    }
  }
  // 模拟人类滚动（已优化）
  async scrollToElement(page: Page, element: ElementHandle): Promise<void> {
    try {
      // 检查元素是否仍然存在
      const isDetached = await element.evaluate((el) => !el.isConnected);
      if (isDetached) {
        throw new Error('目标元素已被移除');
      }

      // 继续滚动
      const { top, height } = await page.evaluate((el) => {
        const rect = el.getBoundingClientRect();
        return { top: rect.top, height: rect.height };
      }, element);

      const viewportHeight = await page.evaluate(() => window.innerHeight);
      const currentScroll = await page.evaluate(() => window.scrollY);
      const targetScroll =
        currentScroll + top - viewportHeight / 2 + height / 2;

      let distance = targetScroll - currentScroll;
      const stepSize = Math.abs(distance) > 500 ? 200 : 50;

      while (Math.abs(distance) > stepSize) {
        const scrollStep = distance > 0 ? stepSize : -stepSize;
        await page.evaluate((step) => window.scrollBy(0, step), scrollStep);
        distance = targetScroll - (await page.evaluate(() => window.scrollY));
        await this.delay(400);
      }

      await page.evaluate(
        (target) => window.scrollTo({ top: target, behavior: 'smooth' }),
        targetScroll,
      );

      if (Math.random() < 0.3) {
        const adjust = Math.random() < 0.5 ? -50 : 50;
        await page.evaluate((adj) => window.scrollBy(0, adj), adjust);
        await this.delay(500);
        await page.evaluate(
          (target) => window.scrollTo({ top: target, behavior: 'smooth' }),
          targetScroll,
        );
      }
    } catch (err) {
      Logger.error(`滚动到元素失败: ${err}`);
    }
  }

  async scrollToSelector(page: Page, selector: string): Promise<void> {
    // 获取要滚动到的元素
    try {
      await page.waitForSelector(selector);
      const element = await page.$(selector);
      if (!element) {
        Logger.error(`无法找到指定的元素: ${selector}`);
        return;
      }

      await this.scrollToElement(page, element);
    } catch (err) {}
  }

  userAgent(): string {
    return (
      randomUseragent.getRandom((ua) => {
        return ua.browserName === 'Chrome' && ua.deviceType === 'desktop';
      }) ||
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    );
  }
  async waitSelector(targetSelector: string, retrytimes: number = 3) {
    if (!this.page) {
      Logger.error('no active page');
      return false;
    }
    Logger.log(
      `waitSelector ${targetSelector} ${this.page.url()} retrytimes:${retrytimes}`,
    );

    while (retrytimes > 0) {
      try {
        Logger.log(`try to wait last time: ${retrytimes}`);
        await this.page.waitForSelector(targetSelector, {
          visible: true,
        }); // Wait up to 10 seconds
        Logger.log(`Element ${targetSelector} is loaded and visible`);
        return true;
      } catch (error) {
        if (retrytimes > 0) {
          await this.page.reload();
          await this.delayRandomly();
          retrytimes -= 1;
          continue;
        }
        Logger.error(`Failed to find ${targetSelector}:`, error);
        await this.browser?.close();
        return false;
      }
    }
  }
}
